react hooks 源码分析

您所在的位置:网站首页 react hooks 源码解析 react hooks 源码分析

react hooks 源码分析

#react hooks 源码分析 | 来源: 网络整理| 查看: 265

1. react hooks简介

react hooks 是react 16.8.0 的新增特性,它可以让你在不编写class的情况下使用state以及其他的一些react特性。

  在过去的react版本中,如果我们想要使用状态管理或者想要在render之后去做一些事情,我们必须使用class组件才能办到。但是现在hooks的出现,使得函数组件也同样可以做到。   hooks实际上是一些以use开头来明名的函数,它就像钩子一样,把函数组件不具备的特性钩进来,使得函数组件也同样可以使用这些特性。   话不多说,下面我们就开始看一下第一个hook的api。

2.useState 使用规则 function User(props) { let [count, setCount] = useState(0); // 这里的count,setCount类似于class组件里的state,setState,我们要改变count这个状态的值,只需要调用setCount这个函数就可以了,它接受一个参数,就是你要更改的值。 let [name, setName] = useState('Mary'); // 你可以在组件内部多次调用useState来创建多个状态变量 return 当前计数: count { setCount(count+1); }}>count+1 }

  useState使得我们可以在函数组件里使用状态,它接受一个参数,就是当前状态的初始值。返回两个变量,第一个变量就是我们的状态变量,第二个就是改变这个状态的函数,类似于class组件里的state和setState。 注意,这里useState返回的是一个数组,所以变量的名字是我们自己任意取的。

useState class state 可以在组件内部多次使用,创建多个状态变量 只有一个state对象 set函数,传进来的参数完全覆盖该状态值 合并state 3.源码分析

  打开源码,我们首先从react.js文件入手,找到useState的源码。

import { useCallback, useContext, useEffect, useImperativeHandle, useDebugValue, useLayoutEffect, useMemo, useReducer, useRef, useState, // 在这里 useResponder, } from './ReactHooks'; // 所以我们要找的源码在这个文件里面

  我们在进到ReactHooks.js文件里看一下

export function useState(initialState: (() => S) | S) { const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); }

  从上述代码可以看出,我们的useState函数是挂到dispatcher对象上面的,那dispatcher对象到底是什么呢,我们再进到resolveDispatcher函数里看一下。   dispatcher对象被赋值为ReactCurrentDispatcher.current,我们在进一步看一下ReactCurrentDispatcher是什么。

import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks'; const ReactCurrentDispatcher = { /** * @internal * @type {ReactComponent} */ current: (null: null | Dispatcher), // current是Dispatcher类型的 }; export default ReactCurrentDispatcher;

  dispatcher我们看到ReactCurrentDispatcher.current被初始化为null,似乎到这里我们什么也没找到。   但我们找到了一条线索,那就是useState其实是挂载ReactCurrentDispatcher.current对象上面的,所以我们只要找到它被赋值的地方就可以了。   但这部分的内容,实际上属于fiber调度的范畴,所以我们就简单提一下,不做过多阐述,实际上真正赋值的地方是在render阶段.

reactFiberHooks.js的renderWithHooks函数中。 文件路径 ReactCurrentDispatcher.current = nextCurrentHook === null ? HooksDispatcherOnMount // 组件挂载阶段 : HooksDispatcherOnUpdate; // 组件更新阶段

  上面代码,当nextCurrentHook为空的时候,被赋值为HooksDispatcherOnMount,不为空的时候被赋值为HooksDispatcherOnUpdate,意思就是说,当组件第一次render,也就是挂载的时候,我们的hook api是在HooksDispatcherOnMount这个对象上的,非首次渲染是在HooksDispatcherOnUpdate对象上的。

const HooksDispatcherOnMount: Dispatcher = { readContext, useCallback: mountCallback, useContext: readContext, useEffect: mountEffect, ... useState: mountState, ... }; const HooksDispatcherOnUpdate: Dispatcher = { readContext, useCallback: updateCallback, useContext: readContext, useEffect: updateEffect, ... useState: updateState, ... };

  所以我们需要分两个分支来看源码。

3.1 mountState

  首先我们需要知道,在组件里,多次调用useState,或者其他hook,那react怎么知道我们当前是哪一个hook呢。其实在react内部,所有的hook api第一次被调用的时候都会先创建一个hook对象,来保存相应的hook信息。然后,这个hook对象,会被加到一个链表上,这样我们每次渲染的时候,只要从这个链表上面依次的去取hook对象,就知道了当前是哪一个hook了。 下面我们就看一下这个hook对象的具体格式。

const hook: Hook = { memoizedState: null, // 缓存当前state的值 baseState: null, // 初始化initState,以及每次dispatch之后的newState queue: null, // update quene baseUpdate: null, //基于哪一个hook进行更新,循环update quene的起点 next: null, // 指向下一个hook };

对于useState来说,memoizedState属性上保存的就是当前hook对应状态变量当前的值,也就是我们获取到的状态变量的值。那这个quene上面保存的是什么呢,我们稍后在解释。   言归正传,我们开始将mountState函数。组件首次渲染的源码,就是mountState这个函数。也就是说首次渲染时useState的源码就是mountState。 那么我们来看看它的实现。

function mountState( initialState: (() => S) | S, ): [S, Dispatch] { const hook = mountWorkInProgressHook(); // 第一步:创建新的hook对象并加到链上,返回workInProgressHook if (typeof initialState === 'function') { initialState = initialState(); } hook.memoizedState = hook.baseState = initialState; // 第二步:获取初始值并初始化hook对象 const queue = (hook.queue = { // 第三步:创建更新队列(update quene),并初始化 last: null, // 最后一次的update对象 dispatch: null, // 更新函数 lastRenderedReducer: basicStateReducer, lastRenderedState: (initialState: any), // 前面最后一次更新的state值,更新的值有可能是函数,函数计算需要用到前一个state的值 }); const dispatch: Dispatch< BasicStateAction, > = (queue.dispatch = (dispatchAction.bind( // 第四步 null, // Flow doesn't know this is non-null, but we do. ((currentlyRenderingFiber: any): Fiber), // 绑定当前fiber和quene queue, ): any)); return [hook.memoizedState, dispatch]; }

第一步,创建hook对象,并将该hook对象加到hook链的末尾。

我们来看一下代码。

function mountWorkInProgressHook(): Hook { const hook: Hook = { // 创建hook对象 memoizedState: null, baseState: null, queue: null, baseUpdate: null, next: null, }; if (workInProgressHook === null) { // 如果是组件内部的第一个hook // This is the first hook in the list firstWorkInProgressHook = workInProgressHook = hook; } else { // 不是第一个hook对象,就直接把新创建的hook对象加到hook链表的末尾 // Append to the end of the list workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook; }

第二步:初始化hook对象的状态值,也就是我们传进来的initState的值。 第三步:创建更新队列,这个队列是更新状态值的时候用的。 第四步:绑定dispatchAction函数。我们可以看到最后一行返回的就是这个函数。也就是说这个函数,其实就是我们改变状态用的函数,就相当于是setState函数。这里它先做了一个绑定当前quene和fiber对象的动作,就是为了在调用setState的时候,知道该更改的是那一个状态的值。   至此,我们就看完了mountState函数。 下面这张图,是我自己画的简易版useState源码的流程图。

useState源码流程图

  那么到这里,我们已经走完了组件首次渲染调用useState时的逻辑。现在,我们已经拿到了我们的状态变量state,那么我们就可以改变这个状态了,也就是调用set函数,这里为了说明方便,我们就直接说setState函数了(实际上你可以随意取名字)。

3.2 dispatchAction

前面已经说过dispatchAction就是我们更改状态值时调用的函数。

function dispatchAction( fiber: Fiber, queue: UpdateQueue, action: A, // 2 ) { ... if(){ rerender逻辑 }else{ const update: Update = { // 第一步 expirationTime, suspenseConfig, action, // 2 eagerReducer: null, eagerState: null, next: null, }; // 第二步:将update加到quene上,更新quene的last为当前update,注意quene是一个环形链表 const last = queue.last; if (last === null) { // This is the first update. Create a circular list. update.next = update; // 环形链 } else { const first = last.next; // 这个last.next是指向第一个update,因为quene是一个环形链表 if (first !== null) { // Still circular. update.next = first; // 使quene变成环形链表 } last.next = update; // 将update加到quene上。 } queue.last = update; // 更新quene的last为当前update } ...

省略无关代码,我们可以看到实际上,dispatchAction这个函数主要做了两件事情。 第一件就是创建了一个update对象,这个对象上面保存了本次更新的相关信息,包括新的状态值action。 第二件,就是将所有的update对象串成了一个环形链表,保存在我们hook对象的queue属性上面。所以我们就知道了queue这个属性的意义,它是保存所有更新行为的地方。 在这里我们可以看到,我们要更改的状态值并没有真的改变,只是被缓存起来了。那么真正改变状态值的地方在哪呢?答案就是在下一次render时,函数组件里的useState又一次被调用了,这个时候才是真的更新state的时机。

3.3 updateState

这里就是我们组件更新时,调用useState时真正走的逻辑了。

function updateState( initialState: (() => S) | S, ): [S, Dispatch] { return updateReducer(basicStateReducer, (initialState: any)); } function updateReducer( reducer: (S, A) => S, // 对于useState来说就是basicStateReducer initialArg: I, init?: I => S, ): [S, Dispatch] { const hook = updateWorkInProgressHook(); // 获取当前正在工作的hook,Q1 const queue = hook.queue; // 更新队列 // The last update in the entire queue const last = queue.last; // 最后一次的update对象 // The last update that is part of the base state. const baseUpdate = hook.baseUpdate; // 上一轮更新的最后一次更新对象 const baseState = hook.baseState; // 上一次的action,现在是初始值 // Find the first unprocessed update. let first; if (baseUpdate !== null) { if (last !== null) { // For the first update, the queue is a circular linked list where // `queue.last.next = queue.first`. Once the first update commits, and // the `baseUpdate` is no longer empty, we can unravel the list. last.next = null; // 因为quene是一个环形链表,所以这里要置空 } first = baseUpdate.next; // 第一次是用的last.next作为第一个需要更新的update,第二次之后就是基于上一次的baseUpdate来开始了(baseUpdate就是上一次的最后一个更新) } else { first = last !== null ? last.next : null; // last.next是第一个update } if (first !== null) { // 没有更新,则不需要执行,直接返回 let newState = baseState; let newBaseState = null; let newBaseUpdate = null; let prevUpdate = baseUpdate; let update = first; let didSkip = false; do { // 循环链表,执行每一次更新 const updateExpirationTime = update.expirationTime; if (updateExpirationTime < renderExpirationTime) { // Priority is insufficient. Skip this update. If this is the first // skipped update, the previous update/state is the new base ... } else { // 正常逻辑 // This update does have sufficient priority. // Process this update. if (update.eagerReducer === reducer) { // 如果是useState,他的reducer就是basicStateReducer // If this update was processed eagerly, and its reducer matches the // current reducer, we can use the eagerly computed state. newState = ((update.eagerState: any): S); } else { const action = update.action; newState = reducer(newState, action); } } prevUpdate = update; update = update.next; } while (update !== null && update !== first); if (!didSkip) { // 不跳过,就更新baseUpdate和baseState newBaseUpdate = prevUpdate; newBaseState = newState; } ... hook.memoizedState = newState; // 更新hook对象 hook.baseUpdate = newBaseUpdate; hook.baseState = newBaseState; queue.lastRenderedState = newState; } const dispatch: Dispatch = (queue.dispatch: any); return [hook.memoizedState, dispatch]; }

updateState做的事情,实际上就是拿到更新队列,循环队列,并根据每一个update对象对当前hook进行状态更新。最后返回最终的结果。

这是我在学习useState源码时的自问自答 1、怎么循环hook对象的,在哪里操作的 (1)从当前fiber对象的memoizedState属性保存着当前组件的第一个hook对象 (2)在每次执行updateState的时候,首先需要获取当前工作中的hook,就是在这里循环的hook (3)hook链是一个环形链吗?不是,是单向链表 在mount阶段,workInProgressHook.next = null,update阶段最后一个hook的next依然是null 是不是说当前fiber对象的memoizedState一直都是第一个hook (462行) 2.Q:更新函数绑定当前hook的地方在哪 A:在dispatchAcion.bind的地方,绑定了fiber和quene 3.Q:更新state时,怎么定位到第一个需要执行的update的 A:基于baseUpdate来开始更新 4.Q:renderWithHooks为什么第一次没有执行 FunctionComponent这个分支? A:renderWithHooks是在组件更新阶段执行的FunctionComponent 5.Q:useState可以放对象吗? A:可以,但是如果setState里的对象还是同一个就不会触发重新渲染

第一次正式的写技术文章,作文水平有限,希望可以帮到大家。

参考 [掘金]» useState源码解析

Youmeng博客 » 阅读源码后,来讲讲React Hooks是怎么实现的



【本文地址】


今日新闻


    推荐新闻


      CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3